// Copyright 2011 George Cook
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "InjectiveFactory.h"
#import "InjectiveContainer.h"
#import <objc/runtime.h>
#import "InjectivePropertyDescription.h"
#import "InjectiveProperty.h"

@interface InjectiveFactory ()

- (void) setPropertyValue:(InjectivePropertyDescription *) propertyDescription onInstance:(id) instance;
- (void) raiseExceptionWithMessage: (NSString *) message;
- (NSArray *) getMetaRequestedInjectionsFor:(id) instance;
- (id) getTypeForProperty:(NSString *) propertyName withInstance:(id) instance;
- (NSString *) getTypeKeyForPropertyName: (NSString *) propertyName withInstance: (id) instance;
- (void) getMetaRequestedInjectionsFor:(Class) clazz andStoreInArray:(NSMutableArray *) injections
				   forOriginalInstance:(id) instance;
@property (retain, readwrite) InjectiveContainer *container;
@end


@implementation InjectiveFactory
@synthesize container;

#pragma mark -
#pragma mark init and dealloc

- (void)dealloc
{
	[container release], container = nil;
	[injectionDescriptionsByClassName release], injectionDescriptionsByClassName = nil;
	[propertyNameTypesByInstanceKey release], propertyNameTypesByInstanceKey = nil;
	[super dealloc];
}

- (id)init
{
	if (self=[super init]) {
		injectionDescriptionsByClassName = [[NSMutableDictionary dictionary] retain];
		propertyNameTypesByInstanceKey = [[NSMutableDictionary dictionary] retain];
	}
	return self;
}

#pragma mark -
#pragma mark Public API

- (id)createInstanceFor: (InjectiveObjectDescription *) objectDescription
{
	
	Class instanceClazz = NSClassFromString(objectDescription.type);
	if (nil == instanceClazz) {
		[self raiseExceptionWithMessage:[NSString stringWithFormat:@"no class was found for : @%", objectDescription]];
	}
	id instance = class_createInstance(instanceClazz, 0);
	if (nil == instance) {
		[self raiseExceptionWithMessage:[NSString stringWithFormat:@"ErroCreatin Instance for : @%", objectDescription]];
	}
	
	instance = [instance init];
	
	[self autoWireDependenciesWith:instance withPropertyDescriptions:objectDescription.propertyDescriptions];
	
	if (nil != objectDescription.initSelectorName) {
		instance = [instance performSelector:NSSelectorFromString(objectDescription.initSelectorName)];
	}
	
	return [instance autorelease];
}


- (void) autoWireDependenciesWith:(id)instance withPropertyDescriptions:(id)propertyDescriptions
{
	NSArray *metaRequestedInjections = [self getMetaRequestedInjectionsFor:instance];
	NSMutableSet *injectedProperties = [[NSMutableSet set] retain];
	for (InjectivePropertyDescription *propertyDescription in metaRequestedInjections) {
		[self setPropertyValue:propertyDescription onInstance:instance];
		[injectedProperties addObject:propertyDescription.propertyName];
	}
	
	for (InjectivePropertyDescription *propertyDescription in propertyDescriptions) {
		if ( [injectedProperties containsObject:propertyDescription.propertyName]) {
			NSLog(@"WARNING: You have set a property %@ with meta injection, AND with property config. The property config is being ignored",
			      propertyDescription.propertyName);
			continue;
		}
		[self setPropertyValue:propertyDescription onInstance:instance];
	}
	[injectedProperties release];
}

- (NSArray *) getMetaRequestedInjectionsFor:(id) instance
{
	NSString *clazzName = NSStringFromClass([instance class]);
	NSMutableArray *injections = [injectionDescriptionsByClassName objectForKey:clazzName];
	if (nil != injections) {
		return injections;
	}
	injections = [[NSMutableArray arrayWithCapacity:10] retain];
	[self getMetaRequestedInjectionsFor:[instance class] andStoreInArray:injections forOriginalInstance:instance];
	
	[injectionDescriptionsByClassName setObject:injections forKey:clazzName];
	return [injections autorelease];
}

- (void) getMetaRequestedInjectionsFor:(Class) clazz andStoreInArray:(NSMutableArray *) injections
				   forOriginalInstance:(id) instance
{
	if ([NSObject class] == clazz) {
		return;
	}
	NSString *clazzName = NSStringFromClass(clazz);
	int unsigned numMethods;
	Method *methods = class_copyMethodList(objc_getMetaClass(
															 [clazzName cStringUsingEncoding:[NSString defaultCStringEncoding]]), &numMethods);
	for (int i = 0; i < numMethods; i++) {
		NSString *methodName = NSStringFromSelector(method_getName(methods[i]));
		NSRange methodNameRange = [methodName rangeOfString:@"InjectiveProperty_"];
		if (methodNameRange.location == 0) {
			NSString *propertyName = [methodName substringWithRange: NSMakeRange (methodNameRange.length, methodName.length - methodNameRange.length)];
			InjectivePropertyDescription *objectDescription = [InjectivePropertyDescription propertyDescriptionWithPropertyName:propertyName ];
			[injections addObject:objectDescription];
		}
	}
	free(methods);
	[self getMetaRequestedInjectionsFor:[clazz superclass] andStoreInArray:injections forOriginalInstance:instance];
}

#pragma mark -
#pragma mark Private methods


- (void) setPropertyValue:(InjectivePropertyDescription *) propertyDescription onInstance:(id) instance
{
	if (![instance respondsToSelector:NSSelectorFromString(propertyDescription.propertyName)]) {
		//WE could go down the root of injecting setters dynamically into the class, but don't reall want to go there..
		[self raiseExceptionWithMessage:
		 [NSString stringWithFormat:@"property wasn't available for propertyDescription %@ on instance %@", propertyDescription, instance]];
	}
	id value = nil;
	if (nil != propertyDescription.value) {
		//TODO: get the actual value from the property description... create a value factory..
		value = propertyDescription.value;
	}
	else if (nil != propertyDescription.type)
	{
		value  = [container getObject:propertyDescription.type];
		//need to set know if we've got type or not
	}
	else if (nil != propertyDescription.idRef)
	{
		value = [container getObjectByID:propertyDescription.idRef];
	}
	else {
		id type = [self getTypeForProperty:propertyDescription.propertyName withInstance:instance];
		value =[container getObject:type];
		if (nil == value) {
			[self raiseExceptionWithMessage:[NSString stringWithFormat:@"Could not find a type in the context for property %@ on class %@",
											 propertyDescription.propertyName, instance]];
		}
		
	}
	if (propertyDescription.sourcePropertyName != nil) {
		if (![value respondsToSelector:NSSelectorFromString(propertyDescription.sourcePropertyName)]) {
			[self raiseExceptionWithMessage:
			 [NSString stringWithFormat:@"sub property wasn't available for propertyDescription", propertyDescription]];
		}
		value = [value valueForKey:propertyDescription.sourcePropertyName];
	}
	[instance setValue:value forKey:propertyDescription.propertyName];
}

- (id) getTypeForProperty:(NSString *) propertyName withInstance:(id) instance
{
	NSString *typeKey = [self getTypeKeyForPropertyName:propertyName withInstance:instance];
	id type = [propertyNameTypesByInstanceKey objectForKey:typeKey];
	if ( nil != type) {
		return type;
	}
	//work out type from the class
	objc_property_t objcProperty = class_getProperty([instance class],
													 [propertyName cStringUsingEncoding:NSUTF8StringEncoding]);
	InjectiveProperty *property = [[InjectiveProperty alloc] initWithObjCProperty:objcProperty];
	type = property.type;
	[property release];
	
	if (nil != type) {
		[propertyNameTypesByInstanceKey setObject:type forKey:typeKey];
	}
	return type;
}

- (NSString *) getTypeKeyForPropertyName: (NSString *) propertyName withInstance: (id) instance
{
	return [propertyName stringByAppendingString:NSStringFromClass([instance class])];
}

- (void) raiseExceptionWithMessage: (NSString *) message
{
	NSString *name = @"INJECTIVE CONTAINER ERROR: Error creatingComponent";
	NSException *ex = [NSException exceptionWithName:name reason:message userInfo:nil];
	[ex raise];
}
@end
